iT邦幫忙

2023 iThome 鐵人賽

DAY 3
1

今天我們來製作useLocalStorage的custom hook,我們希望他可以像useState一樣製作一個方便控制的localstorage的custom hook

EX:

const [state,setState] = useLocalStorage("key","初始value")

不管今天我們要做測試還是自定義hook,我們都要先思考可能需要哪些功能

  1. state 可以直接拿到我們放進去的value或之後設定的value
  2. 初始值第一次渲染也要設定進去localStorage中
  3. setState可以輕鬆幫我們把我想要的值放進去localStorage中,並且不用再寫一次key,例如:
const [theme,setTheme] = useLocalStorage("theme","初始value")
function changeToDarkMode(){
	setTheme("dark")
}
// theme就變成dark,localStorage.getItem("theme")也存入"dark"
  1. setState要記得要把他改成String,state拿出來的時候如果是物件或陣列也要記得恢復

那我們就開始吧

首先寫好useLocalStorage的function,我們需要傳兩個參數,一個是localStorage的key,一個是要傳入的值,首先key一定會是string,所以我們可以先設定key:string ,initialValue 怎麼辦呢?

export function useLocalStorage(key: string, initialValue: ?) {
}

我們這時候有一種選擇是直接用any

export function useLocalStorage(key: string, initialValue: any) {
}

但這時候就有一個顯而易見的缺陷是,它並沒有準確的定義返回值的型別:

any 允許任意型別。但是我們預期的是,陣列中每一項都應該是一開始輸入的 initialValue 的型別

還有~ 如果碰到每個不知道的都用any了,就乾脆直接用Js寫就好了呀,那怎麼辦呢~

這時候我們就要用到typescript好用的泛型拉~

泛型的意思是說,使用者可以自訂型別,後面也會依照型別判斷,很難懂嗎~,用下面範例解釋吧

export function useLocalStorage(key: string, initialValue: any) {
}
// 使用者就可以這樣用了
const [count,setCount]= useLocalStorage<number>("count",0)
const [input,setInput]= useLocalStorage<string>("input","this is an init input")
// 之後要使用setInput裡面就要使用string,setCount裡面只能放number

之後可能還會用到泛型,這個很重要!!! 要記住喔!!!

接下來,我們要建立初始值了

  1. 首先,我們用 const item = localStorage.getItem(key)查詢localStorage這個key有沒有值
  2. return item ? JSON.parse(item) : initialValue; 如果有值就回傳當我們的預設值,如果沒有的話,就拿我們的initialValue來當我們的預設值
  3. 最後放進const [storeState, setStoreState] = useState(readInitValue());存取出初始值
  4. 最後用useCallback避免重複渲染
import { useState, useEffect, useCallback } from "react";

export function useLocalStorage<T>(key: string, initialValue: T) {
  const readInitValue = useCallback(() => {
    try {
			
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (err) {
      console.log(err);
      return initialValue;
    }
  }, [key, initialValue]);
	const [storeState, setStoreState] = useState(readInitValue());
}

接下來我們來寫setLocalStorage吧

因為我們希望他跟平常useState的setState一樣,所以我們也希望可以傳入兩種形式,一種是一般value,一種是useState的updater方式傳入一個function ()⇒{}

我們有以下兩點要注意

  1. 我們要判斷是不是function ,所以我們用value instanceof Function判斷,如果要使用typeof 也可以,但我更傾向用instanceof
  2. 在setLocalStorage記得先用JSON.stringify()轉換成字串,因為localStorage只能存字串
import { useState, useEffect, useCallback } from "react";

export function useLocalStorage<T>(key: string, initialValue: T) {
  function setLocalStorage(value: T | ((prev: T) => T)) {
    try {
      const valueToStore =
        value instanceof Function ? value(storeState) : value;
      setStoreState(valueToStore);
      localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log("Set LocalStorage Error", error);
    }
  }
}

剩最後一點點了~~

我們還剩兩件事

  1. 第一次渲染就將初始值放進localStorage
  2. storeStatesetLocalStorage回傳出去
import { useState, useEffect, useCallback } from "react";

export function useLocalStorage<T>(key: string, initialValue: T) {
  useEffect(() => {
    setLocalStorage(readInitValue());
  }, []);
	// 在 TypeScript 中,as const 用於表示一個 literal expression 是不可變的(即它是唯讀的)以及為了獲得最精確的類型推導
	// 簡單來說,這代表這是一個具有固定長度(2)和固定類型的 tuple。
	// index 0 是 storeState 的類型
	// index 1 是 setLocalStorage 的類型
	// 這個回傳值中不會有第三個或其他的元素
	return [storeState, setLocalStorage] as const;
}

完整程式碼:

import { useState, useEffect, useCallback } from "react";

export function useLocalStorage<T>(key: string, initialValue: T) {
  const readInitValue = useCallback(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (err) {
      console.log(err);
      return initialValue;
    }
  }, [key, initialValue]);

  const [storeState, setStoreState] = useState(readInitValue());
  function setLocalStorage(value: T | ((prev: T) => T)) {
    try {
      const valueToStore =
        value instanceof Function ? value(storeState) : value;
      setStoreState(valueToStore);
      localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log("Set LocalStorage Error", error);
    }
  }

  useEffect(() => {
    setLocalStorage(readInitValue());
  }, []);
	return [storeState, setLocalStorage] as const;
}

如果不太熟的話,可以用useSessionStorage嘗試看看,我會將useSessionStorage的寫法也放在下面(雖然一模一樣拉…)

明天就要開始寫測試了,想試試看的可以今天先寫寫看,明天對答案喔,這個測試我會用三種寫法來教學,敬請期待喔~

useSessionStorage.ts

import { useState, useEffect, useCallback } from "react";

export function useSessionStorage<T>(key: string, initialValue: T) {
  const readInitValue = useCallback(() => {
    try {
      const item = sessionStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (err) {
      console.log(err);
      return initialValue;
    }
  }, [key, initialValue]);

  const [storeState, setStoreState] = useState(readInitValue());
  function setSessionStorage(value: T | ((prev: T) => T)) {
    try {
      const valueToStore =
        value instanceof Function ? value(storeState) : value;
      setStoreState(valueToStore);
      sessionStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log("Set sessionStorage Error", error);
    }
  }

  useEffect(() => {
    setSessionStorage(readInitValue());
  }, []);

  return [storeState, setSessionStorage] as const;
}

上一篇
[Day 2] 就從最基礎的useCounter開始撰寫與測試吧~
下一篇
[Day 4] 開始寫useLocalStorage測試吧
系列文
React進階班,用typescript與jest製作自己的custom hooks庫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言